package com.vdimitr.happycube.puzzle.piece.match;

import com.vdimitr.happycube.puzzle.piece.PuzzlePiece;
import com.vdimitr.happycube.puzzle.piece.PuzzlePieceConstants;
import com.vdimitr.happycube.puzzle.solution.PuzzlePieceSolutionCrossCheck;
import com.vdimitr.happycube.puzzle.solution.PuzzlePieceSolution;
import com.vdimitr.happycube.puzzle.utils.RangeChecker;

/**
 * This class contains the matching strategy for fitting puzzle pieces together
 * and constructing the final cube solution. At first, it tries to match puzzle
 * pieces in a "column", that is placing one after the other in a column
 * starting from the top. When 4 puzzle pieces are fit together, then the 5th
 * puzzle piece is placed on the left and the 6th on the right of the column.
 * Puzzle pieces 5 and 6 must fit with all the puzzle pieces in the column and
 * that is achieved by matching and rotation (clockwise and counter-clockwise).
 * Below is a matrix which represents the positions that the puzzle pieces are
 * check for possible fit. </br> 
 * |5|1|6| </br> 
 * | |2| | </br> 
 * | |3| | </br> 
 * | |4| | </br>
 * 
 * @author vasileios.dimitriadis
 */
public class PuzzlePieceMatcher {

	/**
	 * The minimum position to place a puzzle piece
	 */
	public static final int INCLUSIVE_LOWER_BOUND = 1;

	/**
	 * The maximum position to place a puzzle piece
	 */
	public static final int INCLUSIVE_UPPER_BOUND = PuzzlePieceConstants.NUMBER_OF_PUZZLEPIECES;

	/**
	 * The solution holding the puzzle pieces that form a cube
	 */
	private PuzzlePieceSolution solution = new PuzzlePieceSolution();

	/**
	 * Matching strategy to fit puzzle pieces top to bottom
	 */
	private PuzzlePieceTopToBottomMatcher topToBottomMatcher = new PuzzlePieceTopToBottomMatcher();

	/**
	 * Matching strategy to fit a puzzle piece on the right side
	 */
	private PuzzlePieceRightToRowMatcher rightToRowMatcher = new PuzzlePieceRightToRowMatcher();

	/**
	 * Matching strategy to fit a puzzle piece on the left side
	 */
	private PuzzlePieceLeftToRowMatcher leftToRowMatcher = new PuzzlePieceLeftToRowMatcher();

	/**
	 * 
	 * @return the solution constructed
	 */
	public PuzzlePieceSolution getSolution() {
		return this.solution;
	}

	/**
	 * Locates the puzzle piece in the solution at the specified position
	 * 
	 * @param position
	 * @return the puzzle piece in the specified position
	 */
	public PuzzlePiece getPuzzlePieceAtPosition(int position) {
		return this.solution.getPuzzlePieceAtPosition(position);
	}

	/**
	 * Tries to fit a puzzle piece at the specified position by using matching
	 * strategies which determine if the puzzle piece is a possible candidate
	 * for the solution. If so then the puzzle piece is added to the solution.
	 * 
	 * @param puzzlePiece
	 *            a candidate puzzle piece to fit
	 * @param position
	 *            the position which it should be put
	 * @return true if the puzzle piece can be fitted in the specified position
	 *         based on the matching rules
	 */
	public boolean puzzlePieceFits(PuzzlePiece puzzlePiece, int position) {
		positionRangeCheck(position);
		boolean puzzliePieceFits = false;
		switch (position) {
		case 1: // p1 always fits
			puzzliePieceFits = true;
			break;
		case 2: // p2 should fit with p1
			puzzliePieceFits = isPuzzlePieceSuitable(
					this.solution.getPuzzlePieceAtPosition(1), puzzlePiece);
			break;
		case 3: // p3 should fit with p2
			puzzliePieceFits = isPuzzlePieceSuitable(
					this.solution.getPuzzlePieceAtPosition(2), puzzlePiece);
			break;
		case 4: // p4 should fit with p3 and p1
			puzzliePieceFits = isPuzzlePieceSuitable(
					this.solution.getPuzzlePieceAtPosition(3), puzzlePiece)
					&& isPuzzlePieceSuitable(puzzlePiece,
							this.solution.getPuzzlePieceAtPosition(1));
			break;
		case 5: // p5 should fit with p1-p4
			puzzliePieceFits = isPuzzlePieceSuitableInTopLeftCorner(puzzlePiece);
			break;
		case 6: // p6 should fit with p1-p4
			puzzliePieceFits = isPuzzlePieceSuitableInTopRightCorner(puzzlePiece);
			break;
		}
		if (puzzliePieceFits) {
			this.solution.setPuzzlePieceAtPosition(puzzlePiece, position);
		}
		return puzzliePieceFits;
	}

	/**
	 * Checks if two puzzle pieces can be fitted together in a top to bottom
	 * manner
	 * 
	 * @param topPuzzlePiece
	 *            a candidate puzzle piece
	 * @param bottomPuzzlePiece
	 *            a candidate puzzle piece
	 * @return true if the puzzle piece can be fitted together
	 */
	private boolean isPuzzlePieceSuitable(PuzzlePiece topPuzzlePiece,
			PuzzlePiece bottomPuzzlePiece) {
		return this.topToBottomMatcher.puzzlePiecesMatchTopToBottom(
				topPuzzlePiece, bottomPuzzlePiece);
	}

	/**
	 * Checks if the puzzle piece can be fitted to the right side
	 * 
	 * @param puzzlePiece
	 *            a candidate puzzle piece
	 * @return true if the puzzle piece matches the right side of the cube
	 */
	private boolean isPuzzlePieceSuitableInTopRightCorner(
			PuzzlePiece puzzlePiece) {
		PuzzlePiece[] puzzlePieces = this.solution.getMiddlePuzzlePieceColumn();
		return this.rightToRowMatcher.puzzlePiecesMatchRightToRow(puzzlePiece,
				puzzlePieces);
	}

	/**
	 * Checks if the puzzle piece can be fitted to the left side
	 * 
	 * @param puzzlePiece
	 *            a candidate puzzle piece
	 * @return true if the puzzle piece matches the left side of the cube
	 */
	private boolean isPuzzlePieceSuitableInTopLeftCorner(PuzzlePiece puzzlePiece) {
		PuzzlePiece[] puzzlePieces = this.solution.getMiddlePuzzlePieceColumn();
		return this.leftToRowMatcher.puzzlePiecesMatchLeftToRow(puzzlePiece,
				puzzlePieces);
	}

	/**
	 * Last checkpoint for verifying that the constructed solution is correct
	 * 
	 * @return true if the constructed solution forms a cube
	 */
	public boolean isSolutionValid() {
		if (this.solution.isComplete()) {
			PuzzlePieceSolutionCrossCheck crossCheck = new PuzzlePieceSolutionCrossCheck();
			return crossCheck.isValid(this.solution);
		} else {
			return false;
		}
	}

	/**
	 * Checks if the position is valid
	 * 
	 * @param position
	 *            the position we are trying to put a puzzle piece
	 * @throws IllegalArgumentException
	 *             if the position is out of bounds
	 */
	private void positionRangeCheck(int position) {
		if (RangeChecker.isOutOfRange(INCLUSIVE_LOWER_BOUND,
				INCLUSIVE_UPPER_BOUND, position)) {
			throw new IllegalArgumentException("Position: " + position
					+ " is not valid. Mmust be between "
					+ INCLUSIVE_LOWER_BOUND + " and  " + INCLUSIVE_UPPER_BOUND);
		}
	}

}
